package controllers

/*
Copyright 2021-2025 The k8gb Contributors.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

Generated by GoLic, for more details see: https://github.com/AbsaOSS/golic
*/

import (
	"context"
	"fmt"
	"strconv"

	k8gbv1beta1 "github.com/k8gb-io/k8gb/api/v1beta1"
	"github.com/k8gb-io/k8gb/controllers/resolver"
	"github.com/k8gb-io/k8gb/controllers/utils"
	corev1 "k8s.io/api/core/v1"
	netv1 "k8s.io/api/networking/v1"
	k8serrors "k8s.io/apimachinery/pkg/api/errors"
	"k8s.io/apimachinery/pkg/runtime"
	"sigs.k8s.io/controller-runtime/pkg/client"
	"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
	"sigs.k8s.io/controller-runtime/pkg/reconcile"
)

type Handler interface {
	Handle(client.Object) []reconcile.Request
}

// IngressHandler
type IngressHandler struct {
	client  client.Client
	scheme  *runtime.Scheme
	context context.Context
}

func NewIngressHandler(ctx context.Context, client client.Client, scheme *runtime.Scheme) *IngressHandler {
	return &IngressHandler{
		context: ctx,
		client:  client,
		scheme:  scheme,
	}
}

func (g *IngressHandler) Handle(ing client.Object) []reconcile.Request {

	// following lines filters only Ingresses with strategyAnnotation which are not owned by GSLB
	// filtering out Ingress without k8gb strategy
	if !g.isK8gbAnnotated(ing) {
		return nil
	}

	// filtering out Ingress which is owned by GSLB
	if g.isOwnedByGSLB(ing) {
		return nil
	}

	// gslb created from ingress will run standalone reconciliation cycle automatically
	_ = g.createGslbFromIngress(ing, g.scheme)

	return nil
}

func (g *IngressHandler) isOwnedByGSLB(obj client.Object) bool {
	for _, r := range obj.GetOwnerReferences() {
		if r.Kind == "Gslb" {
			return true
		}
	}
	return false
}

func (g *IngressHandler) isK8gbAnnotated(obj client.Object) bool {
	annotations := obj.GetAnnotations()
	_, found := annotations[strategyAnnotation]
	return found
}

func (g *IngressHandler) createGslbFromIngress(ing client.Object, scheme *runtime.Scheme) *k8gbv1beta1.Gslb {
	log.Warn().
		Str("ingress", ing.GetName()).
		Msg("Configuration GSLB resources via Ingress annotations is deprecated. " +
			"This feature will be removed in k8gb v0.17. Please explicitly define a GSLB resource instead")

	strategy := ing.GetAnnotations()[strategyAnnotation]
	objectKey := client.ObjectKey{Namespace: ing.GetNamespace(), Name: ing.GetName()}
	log.Info().
		Str("annotation", fmt.Sprintf("(%s:%s)", strategyAnnotation, strategy)).
		Str("ingress", ing.GetName()).
		Msg("Detected strategy annotation on ingress")

	ingressToReuse := &netv1.Ingress{}
	err := g.client.Get(context.Background(), objectKey, ingressToReuse)
	if err != nil {
		log.Info().
			Str("ingress", objectKey.Name).
			Msg("Ingress doesn't exist anymore. Skipping Gslb creation...")
		return nil
	}

	gslb, isNew, err := g.getGslb(ing)
	if err != nil {
		log.Err(err).
			Str("gslb", ing.GetName()).
			Msg("Cannot build the Gslb object from ingress")
		return nil
	}
	err = controllerutil.SetControllerReference(ingressToReuse, gslb, scheme)
	if err != nil {
		log.Err(err).
			Str("ingress", ingressToReuse.Name).
			Str("gslb", gslb.Name).
			Msg("Cannot set the Ingress as the owner of the Gslb")
		return nil
	}
	// create
	if isNew {
		log.Info().
			Str("gslb", gslb.Name).
			Msg(fmt.Sprintf("Creating a new Gslb out of Ingress with '%s' annotation", strategyAnnotation))
		err = g.client.Create(context.Background(), gslb)
		if err != nil {
			log.Err(err).Msg("Gslb creation failed")
			return nil
		}
		return gslb
	}
	// update
	log.Info().
		Str("gslb", gslb.Name).
		Str("namespace", objectKey.Namespace).
		Msg(fmt.Sprintf("Updating a Gslb out of Ingress %s", objectKey.Name))
	err = g.client.Update(context.Background(), gslb)
	if err != nil {
		log.Err(err).Msg("Gslb update failed")
		return nil
	}
	return gslb
}

func (g *IngressHandler) getGslb(obj client.Object) (*k8gbv1beta1.Gslb, bool, error) {
	gslb := &k8gbv1beta1.Gslb{}
	objectKey := client.ObjectKey{Namespace: obj.GetNamespace(), Name: obj.GetName()}
	isNew := false
	err := g.client.Get(context.Background(), objectKey, gslb)
	if err != nil {
		if !k8serrors.IsNotFound(err) {
			log.Err(err).
				Str("gslb", objectKey.Name).
				Str("namespace", objectKey.Namespace).
				Msg("reading Gslb object failed")
			return nil, false, err
		}
		// is not found
		isNew = true
		log.Info().
			Str("gslb", objectKey.Name).
			Str("namespace", objectKey.Namespace).
			Msg("Gslb doesn't exist, creating...")
	}

	strategyObj, err := g.parseStrategySpec(obj.GetAnnotations())
	if err != nil {
		log.Err(err).
			Str("gslb", obj.GetName()).
			Msg("can't parse Gslb strategy")
		return nil, isNew, err
	}
	strategyObj.Weight = gslb.Spec.Strategy.Weight

	gslb.ObjectMeta.Name = obj.GetName()
	gslb.ObjectMeta.Namespace = obj.GetNamespace()
	if val, found := obj.GetAnnotations()[utils.ExternalIPsAnnotation]; found {
		if gslb.ObjectMeta.Annotations == nil {
			gslb.ObjectMeta.Annotations = make(map[string]string)
		}
		gslb.ObjectMeta.Annotations[utils.ExternalIPsAnnotation] = val
	}
	// migration to resourceRef
	gslb.Spec = k8gbv1beta1.GslbSpec{
		ResourceRef: k8gbv1beta1.ResourceRef{
			ObjectReference: corev1.ObjectReference{
				Name:       obj.GetName(),
				Kind:       "Ingress",
				APIVersion: "networking.k8s.io/v1",
			},
		},
		// detaching ingress spec
		Ingress:  k8gbv1beta1.IngressSpec{},
		Strategy: strategyObj,
	}
	return gslb, isNew, nil
}

// parseStrategySpec parses strategy specifications from annotations for Ingress resources
func (g *IngressHandler) parseStrategySpec(annotations map[string]string) (result k8gbv1beta1.Strategy, err error) {
	toInt := func(k string, v string) (int, error) {
		intValue, err := strconv.Atoi(v)
		if err != nil {
			return -1, fmt.Errorf("can't parse annotation value %s to int for key %s", v, k)
		}
		return intValue, nil
	}

	result = k8gbv1beta1.Strategy{}

	if value, found := annotations[strategyAnnotation]; found {
		result.Type = value
	} else {
		return result, fmt.Errorf("annotation %s not found", strategyAnnotation)
	}

	result.DNSTtlSeconds = resolver.DefaultTTLSeconds
	if value, found := annotations[dnsTTLSecondsAnnotation]; found {
		if result.DNSTtlSeconds, err = toInt(dnsTTLSecondsAnnotation, value); err != nil {
			return result, err
		}
	}
	result.PrimaryGeoTag = annotations[primaryGeoTagAnnotation]

	if result.Type == resolver.FailoverStrategy {
		if len(result.PrimaryGeoTag) == 0 {
			return result, fmt.Errorf("%s strategy requires annotation %s", resolver.FailoverStrategy, primaryGeoTagAnnotation)
		}
	}

	return result, nil
}
